package compiler.semanal;

import compiler.abstree.AbsVisitor;
import compiler.abstree.tree.AbsAlloc;
import compiler.abstree.tree.AbsArrayType;
import compiler.abstree.tree.AbsAssignStmt;
import compiler.abstree.tree.AbsAtomConst;
import compiler.abstree.tree.AbsAtomType;
import compiler.abstree.tree.AbsBinExpr;
import compiler.abstree.tree.AbsBlockStmt;
import compiler.abstree.tree.AbsCallExpr;
import compiler.abstree.tree.AbsConstDecl;
import compiler.abstree.tree.AbsDecl;
import compiler.abstree.tree.AbsDeclName;
import compiler.abstree.tree.AbsDecls;
import compiler.abstree.tree.AbsExprStmt;
import compiler.abstree.tree.AbsForStmt;
import compiler.abstree.tree.AbsFunDecl;
import compiler.abstree.tree.AbsIfStmt;
import compiler.abstree.tree.AbsNilConst;
import compiler.abstree.tree.AbsPointerType;
import compiler.abstree.tree.AbsProcDecl;
import compiler.abstree.tree.AbsProgram;
import compiler.abstree.tree.AbsRecordType;
import compiler.abstree.tree.AbsStmt;
import compiler.abstree.tree.AbsStmts;
import compiler.abstree.tree.AbsTree;
import compiler.abstree.tree.AbsTypeDecl;
import compiler.abstree.tree.AbsTypeName;
import compiler.abstree.tree.AbsUnExpr;
import compiler.abstree.tree.AbsValExpr;
import compiler.abstree.tree.AbsValExprs;
import compiler.abstree.tree.AbsValName;
import compiler.abstree.tree.AbsVarDecl;
import compiler.abstree.tree.AbsWhileStmt;
import compiler.report.Report;
import compiler.semanal.type.*;

public class SemTypeChecker implements AbsVisitor {

	public boolean error=false;
	
	private void setStandardFuntionsType() {
		String[] stdFun = {"putch", "putint", "getch", "getint", "ord", "chr", "free"};
		
		for(String fun:stdFun) {
			AbsDecl decl=SemTable.fnd(fun);
			
			if(decl instanceof AbsFunDecl)
				this.visit((AbsFunDecl)decl);
			else
				this.visit((AbsProcDecl)decl);
		}
	}
	
	@Override
	public void visit(AbsAlloc acceptor) 
	{
		acceptor.type.accept(this);
		SemType allocType = SemDesc.getActualType(acceptor.type);
		
		if(allocType instanceof SemAtomType) {
			SemAtomType type = (SemAtomType)allocType;
			if(type.type == SemAtomType.INT) {
				SemDesc.setActualType(acceptor, type);
				return;
			}
		}
		Report.warning("Operand ni tipa integer!", acceptor.begLine, acceptor.begColumn);
		SemDesc.setActualType(acceptor, new SemTypeError());
		acceptor.error=true;
		error=true;
	}

	@Override
	public void visit(AbsArrayType acceptor) 
	{
		ConstEvaluator eval = new ConstEvaluator();
		
		acceptor.hiBound.accept(this);
		acceptor.loBound.accept(this);
		acceptor.hiBound.accept(eval);
		acceptor.loBound.accept(eval);
		
		int hi=SemDesc.getActualConst(acceptor.hiBound);		
		int lo=SemDesc.getActualConst(acceptor.loBound);
		
		if(hi<lo) {
			Report.warning("Napacne meje tabel!", acceptor.begLine, acceptor.begColumn);
			SemDesc.setActualType(acceptor, new SemTypeError());
			acceptor.error=true;
			error=true;
		}

		
		acceptor.type.accept(this);
		SemType hiType  = SemDesc.getActualType(acceptor.hiBound);
		SemType loType = SemDesc.getActualType(acceptor.loBound);
				
		SemType elType = SemDesc.getActualType(acceptor.type);
		if(hiType instanceof SemAtomType && loType instanceof SemAtomType) {
			SemAtomType _hiType = (SemAtomType)hiType;
			SemAtomType _loType = (SemAtomType)loType;
			if(_hiType.type== SemAtomType.INT && _loType.type == SemAtomType.INT) {
				SemArrayType arrayType = new SemArrayType(elType, lo, hi);
				SemDesc.setActualType(acceptor, arrayType);
				return;
			}			
		}
		Report.warning("Operanda nista tipa integer!", acceptor.begLine, acceptor.begColumn);
		SemDesc.setActualType(acceptor, new SemTypeError());
		acceptor.error=true;
		error=true;
	}

	@Override
	public void visit(AbsAssignStmt acceptor) 
	{
		acceptor.srcExpr.accept(this);
		acceptor.dstExpr.accept(this);
		SemType srcType = SemDesc.getActualType(acceptor.srcExpr);
		SemType dstType = SemDesc.getActualType(acceptor.dstExpr);
							
		if(srcType instanceof SemSubprogramType) {
			srcType = ((SemSubprogramType) srcType).getResultType();
		}
		
		if(dstType instanceof SemSubprogramType)
			dstType = ((SemSubprogramType) dstType).getResultType();
		
		if(srcType instanceof SemAtomType || srcType instanceof SemPointerType) {
			if(srcType.coercesTo(dstType))
				return;
		}
		Report.warning("Tipa v izrazu se ne ujemata!", acceptor.begLine, acceptor.begColumn);
		SemDesc.setActualType(acceptor, new SemTypeError());
		acceptor.error=true;
		error=true;
	}

	@Override
	public void visit(AbsAtomConst acceptor) 
	{
		SemDesc.setActualType(acceptor, new SemAtomType(acceptor.type));
	}

	@Override
	public void visit(AbsAtomType acceptor) 
	{
		SemDesc.setActualType(acceptor, new SemAtomType(acceptor.type));
	}

	@Override
	public void visit(AbsBinExpr acceptor) 
	{
		
		acceptor.fstExpr.accept(this);
		
		if(!(acceptor.oper  == AbsBinExpr.RECACCESS))
			acceptor.sndExpr.accept(this);
		
		SemType fstType = SemDesc.getActualType(acceptor.fstExpr);
		SemType sndType = SemDesc.getActualType(acceptor.sndExpr);
		
		if(fstType instanceof SemSubprogramType)
			fstType = ((SemSubprogramType) fstType).getResultType();
			
		if(sndType instanceof SemSubprogramType)
			sndType = ((SemSubprogramType) sndType).getResultType();		
		
		switch(acceptor.oper){
			case AbsBinExpr.ADD:
			case AbsBinExpr.SUB:
			case AbsBinExpr.MUL:
			case AbsBinExpr.DIV:			
				if(fstType instanceof SemAtomType && sndType instanceof SemAtomType) {					
					if(	((SemAtomType) fstType).type == SemAtomType.INT &&
						((SemAtomType) sndType).type == SemAtomType.INT) {
						SemDesc.setActualType(acceptor, new SemAtomType(SemAtomType.INT));
						return;
					}
				}
				Report.warning("Operanda nista tipa int!", acceptor.begLine, acceptor.begColumn);
				SemDesc.setActualType(acceptor, new SemTypeError());
				acceptor.error=true;
				error=true;
				break;
			case AbsBinExpr.AND:	
			case AbsBinExpr.OR:			
				if(fstType instanceof SemAtomType && sndType instanceof SemAtomType) {					
					if(	((SemAtomType) fstType).type == SemAtomType.BOOL &&
						((SemAtomType) sndType).type == SemAtomType.BOOL) {
						SemDesc.setActualType(acceptor, new SemAtomType(SemAtomType.BOOL));
						return;
					}
				}
				Report.warning("Operanda nista tipa boolean!", acceptor.begLine, acceptor.begColumn);
				SemDesc.setActualType(acceptor, new SemTypeError());
				acceptor.error=true;
				error=true;
				break;
			case AbsBinExpr.EQU:
			case AbsBinExpr.NEQ:
			case AbsBinExpr.LTH:
			case AbsBinExpr.GTH:
			case AbsBinExpr.LEQ:
			case AbsBinExpr.GEQ:				
				//preverimo, ce sta oba izraza istega tipa
				if( fstType.coercesTo(sndType)) {
					if(fstType instanceof SemAtomType) {
						SemDesc.setActualType(acceptor, new SemAtomType(SemAtomType.BOOL));
						return;
					}
					else if(fstType instanceof SemPointerType) {
						SemDesc.setActualType(acceptor, new SemAtomType(SemAtomType.BOOL));
						return;
					}
				}
				Report.warning("Tipa v izrazu se ne ujemata!", acceptor.begLine, acceptor.begColumn);
				SemDesc.setActualType(acceptor, new SemTypeError());
				acceptor.error=true;
				error=true;
				break;
			case AbsBinExpr.RECACCESS:
				if(fstType instanceof SemRecordType) {
					int index=0;
					if(	acceptor.sndExpr instanceof AbsValName && 
						(index=checkRecordItemName((AbsValName) acceptor.sndExpr, (SemRecordType) fstType)) > -1) {
						SemRecordType recType = (SemRecordType)fstType;
						sndType = recType.getFieldType(index);
						SemDesc.setActualType(acceptor.sndExpr, sndType);
						SemDesc.setActualType(acceptor, sndType);
						return;
					}
					
					if(index == -1)
						Report.warning(((AbsValName)acceptor.fstExpr).name +" nima atributa "+ ((AbsValName)acceptor.sndExpr).name +"!",
										acceptor.begLine, acceptor.begColumn);
				}
				Report.warning("Napaka tipa pri dostopu do elementa recorda.", acceptor.begLine, acceptor.begColumn);
				SemDesc.setActualType(acceptor, new SemTypeError());
				acceptor.error=true;
				error=true;
				break;
			case AbsBinExpr.ARRACCESS:
				if(fstType instanceof SemArrayType && sndType instanceof SemAtomType) {
					if(((SemAtomType) sndType).type == SemAtomType.INT) {
						SemDesc.setActualType(acceptor, ((SemArrayType) fstType).type);
						return;
					}
				}
				Report.warning("Napaka tipa pri dostopu do elementa arraya.", acceptor.begLine, acceptor.begColumn);
				SemDesc.setActualType(acceptor, new SemTypeError());
				acceptor.error=true;
				error=true;
		}
	}

	@Override
	public void visit(AbsBlockStmt acceptor) 
	{
		acceptor.stmts.accept(this);
	}

	@Override
	public void visit(AbsCallExpr acceptor) 
	{
		acceptor.args.accept(this);
		
		acceptor.name.accept(this);
		SemType declType = SemDesc.getActualType(acceptor.name);
		declType = ((SemSubprogramType)declType).getResultType();
		SemDesc.setActualType(acceptor, declType);
	}

	@Override
	public void visit(AbsConstDecl acceptor) 
	{
		//ce nismo poracunal ze pri evaluaciji konstant
		if(SemDesc.getActualType(acceptor) == null) 
		{
			acceptor.value.accept(this);
			SemType valType = SemDesc.getActualType(acceptor.value);
			SemDesc.setActualType(acceptor, valType);			
		}
	}

	@Override
	public void visit(AbsDeclName acceptor) 
	{
		//DO NOTHING
	}

	@Override
	public void visit(AbsDecls acceptor) 
	{
		//testiranje ciklov;
		acceptor.accept(new CycleChecker());	
		
		for(AbsDecl decl: acceptor.decls) {
			decl.accept(this);
		}
	}

	@Override
	public void visit(AbsExprStmt acceptor) 
	{
		acceptor.expr.accept(this);

		if(acceptor.expr instanceof AbsCallExpr) {
			AbsTree decl = SemDesc.getNameDecl( ((AbsCallExpr)acceptor.expr).name);			
			if(decl instanceof AbsProcDecl) {
				return;
			}
		}
		Report.warning("Stavek je lahko samo klic procedure!", acceptor.begLine, acceptor.begColumn);
		SemDesc.setActualType(acceptor, new SemTypeError());
		acceptor.error=true;
		error=true;
	}

	@Override
	public void visit(AbsForStmt acceptor) 
	{
		acceptor.hiBound.accept(this);
		acceptor.loBound.accept(this);
		acceptor.name.accept(this);
		acceptor.stmt.accept(this);
		
		SemType hiType  = SemDesc.getActualType(acceptor.hiBound);
		SemType loType  = SemDesc.getActualType(acceptor.loBound);
		SemType varType = SemDesc.getActualType(acceptor.name);

		if(hiType instanceof SemAtomType && loType instanceof SemAtomType && varType instanceof SemAtomType) {
			SemAtomType _hiType = (SemAtomType)hiType;
			SemAtomType _loType = (SemAtomType)loType;
			SemAtomType _varType = (SemAtomType)varType;
			if(_hiType.type == SemAtomType.INT && _loType.type == SemAtomType.INT && _varType.type == SemAtomType.INT)
				return;
		}
		Report.warning("Napaka tipa pri pogojih!", acceptor.begLine, acceptor.begColumn);
		SemDesc.setActualType(acceptor, new SemTypeError());
		acceptor.error=true;
		error=true;
	}

	@Override
	public void visit(AbsFunDecl acceptor) 
	{
		acceptor.pars.accept(this);
		acceptor.decls.accept(this);
		acceptor.type.accept(this);
		
		
		SemType resultType = SemDesc.getActualType(acceptor.type);
		SemSubprogramType funcType = new SemSubprogramType(resultType);
		
		for(AbsDecl decl: acceptor.pars.decls) {
			SemType declType = SemDesc.getActualType(decl);
			funcType.addParType(declType);
		}
		
		SemDesc.setActualType(acceptor, funcType);
		//ker se lahko v telesu funkcije sklicujes na isto funkcijo je naprej treba dolociti tip funkcije sele nato se gre v njeno telo
		acceptor.stmt.accept(this);	
	}

	@Override
	public void visit(AbsIfStmt acceptor) 
	{
		acceptor.cond.accept(this);
		acceptor.thenStmt.accept(this);
		acceptor.elseStmt.accept(this);
		
		SemType condType = SemDesc.getActualType(acceptor.cond);
		
		if(condType instanceof SemAtomType) {
			if(((SemAtomType) condType).type == SemAtomType.BOOL)
				return;
		}
		Report.warning("Napaka tipa v pogoju!", acceptor.begLine, acceptor.begColumn);
		SemDesc.setActualType(acceptor, new SemTypeError());
		acceptor.error=true;
		error=true;
	}

	@Override
	public void visit(AbsNilConst acceptor) 
	{
		SemDesc.setActualType(acceptor, new SemPointerType(null));
	}

	@Override
	public void visit(AbsPointerType acceptor) 
	{
		acceptor.type.accept(this);
		SemType dataType = SemDesc.getActualType(acceptor.type);
		SemPointerType pointerType = new SemPointerType(dataType);
		SemDesc.setActualType(acceptor, pointerType);

	}

	@Override
	public void visit(AbsProcDecl acceptor) 
	{
		acceptor.pars.accept(this);
		acceptor.decls.accept(this);
		
		SemSubprogramType procType = new SemSubprogramType(new SemAtomType(SemAtomType.VOID));
		
		for(AbsDecl decl: acceptor.pars.decls) {
			SemType declType = SemDesc.getActualType(decl);
			procType.addParType(declType);
		}
		SemDesc.setActualType(acceptor, procType);
		acceptor.stmt.accept(this);
	}

	@Override
	public void visit(AbsProgram acceptor) 
	{	
		this.setStandardFuntionsType();
		acceptor.decls.accept(this);
		acceptor.stmt.accept(this);
	}

	@Override
	public void visit(AbsRecordType acceptor) 
	{
		SemRecordType recordType = new SemRecordType();
		SemDesc.setActualType(acceptor, recordType);
		acceptor.fields.accept(this);
		
		for(AbsDecl decl : acceptor.fields.decls) {
			AbsDeclName declName  = ((AbsVarDecl)decl).name;
			SemType declType = SemDesc.getActualType(decl);
			recordType.addField(declName, declType);
		}
	}

	@Override
	public void visit(AbsStmts acceptor) {
		for(AbsStmt stmt : acceptor.stmts)
			stmt.accept(this);
	}

	@Override
	public void visit(AbsTypeDecl acceptor) 
	{
		if(SemDesc.getActualType(acceptor) == null) 
		{
			acceptor.type.accept(this);

			SemType type = SemDesc.getActualType(acceptor.type);
			SemDesc.setActualType(acceptor, type);
		}
	}

	@Override
	public void visit(AbsTypeName acceptor) 
	{
		//imenu oz. uporabi tipa se nastavi tip njegove deklaracije
		AbsDecl decl = SemDesc.getNameDecl(acceptor);
		SemType declType = SemDesc.getActualType(decl);
		
		if(declType == null) {
			decl.accept(this);
			declType = SemDesc.getActualType(decl);
		}
		SemDesc.setActualType(acceptor, declType);
	}

	@Override
	public void visit(AbsUnExpr acceptor) 
	{
		acceptor.expr.accept(this);
		
		SemType exprType = SemDesc.getActualType(acceptor.expr);

		if(exprType instanceof SemSubprogramType)
			exprType = ((SemSubprogramType) exprType).getResultType();
		
		switch(acceptor.oper) {
			case AbsUnExpr.ADD:
			case AbsUnExpr.SUB:					
				if(exprType instanceof SemAtomType) {
					SemAtomType intType = (SemAtomType)exprType;
					if(intType.type == SemAtomType.INT) {
						SemDesc.setActualType(acceptor, intType);
						return;
					}					
				}
				Report.warning("Operand ni tipa integer!", acceptor.begLine, acceptor.begColumn);
				SemDesc.setActualType(acceptor, new SemTypeError());
				acceptor.error=true;
				error=true;
				break;
			case AbsUnExpr.NOT:
				if(exprType instanceof SemAtomType) {
					SemAtomType boolType = (SemAtomType)exprType;
					if(boolType.type == SemAtomType.BOOL) {
						SemDesc.setActualType(acceptor, boolType);
						return;
					}
				}
				Report.warning("Operand ni tipa boolean!", acceptor.begLine, acceptor.begColumn);
				SemDesc.setActualType(acceptor, new SemTypeError());
				acceptor.error=true;
				error=true;
				break;
			case AbsUnExpr.MEM:
				//argument prefiksnega pointerja je poljubnega tipa, zato tukaj ni potrebno preverjanje
				SemDesc.setActualType(acceptor, new SemPointerType(exprType));
				break;
			case AbsUnExpr.VAL:
				if(exprType instanceof SemPointerType) {
					SemDesc.setActualType(acceptor, ((SemPointerType) exprType).type);
				}
				else {
					Report.warning("Operand ni tipa pointer!", acceptor.begLine, acceptor.begColumn);
					SemDesc.setActualType(acceptor, new SemTypeError());
					acceptor.error=true;
					error=true;
				}
		}
	}

	@Override
	public void visit(AbsValExprs acceptor) 
	{
		for(AbsValExpr ve : acceptor.exprs)
			ve.accept(this);
	}

	@Override
	public void visit(AbsValName acceptor) 
	{
		AbsDecl decl = SemDesc.getNameDecl(acceptor);
		SemType declType;
		
		declType = SemDesc.getActualType(decl);
		
		if(declType == null) {
			decl.accept(this);
			declType= SemDesc.getActualType(decl);
		}
				
		SemDesc.setActualType(acceptor, declType);
	}

	@Override
	public void visit(AbsVarDecl acceptor) 
	{
		acceptor.type.accept(this);

		SemType type = SemDesc.getActualType(acceptor.type);
		SemDesc.setActualType(acceptor, type);
	}

	@Override
	public void visit(AbsWhileStmt acceptor) 
	{
		acceptor.cond.accept(this);
		acceptor.stmt.accept(this);
		SemType condType = SemDesc.getActualType(acceptor.cond);
		
		if(condType instanceof SemAtomType) {
			if(((SemAtomType) condType).type == SemAtomType.BOOL)
				return;
		}
		Report.warning("Napaka tipa v pogoju!", acceptor.begLine, acceptor.begColumn);
		SemDesc.setActualType(acceptor, new SemTypeError());
		acceptor.error=true;
		error=true;
	}
	
	private int checkRecordItemName(AbsValName sndExpr, SemRecordType fstType) 
	{
		int numOfField = fstType.getNumFields();
		for(int i = 0; i < numOfField; i++) {
			if(fstType.getFieldName(i).name.equals(sndExpr.name))
				return i;
		}
		
		return -1;
	}

}
